-- -------------------------------------------------------------------------
-- UI/layout.lua
-- Layout System
-- -------------------------------------------------------------------------

local layout = {}

local state = require("ClickUl.UI.state")
local renderer = require("ClickUl.UI.renderer")
local input_module = require("ClickUl.UI.input")
local widgets = nil

-- [Lua 5.4] <const> for function references
local math_floor <const> = math.floor
local math_max <const> = math.max
local math_min <const> = math.min
local math_exp <const> = math.exp
local os_clock <const> = os.clock
local table_remove <const> = table.remove
local ipairs <const> = ipairs
local type <const> = type
local string_find <const> = string.find

local failed_images = {}
local failed_sidebar_textures = {}

local anim_color_cache = {r=255, g=255, b=255, a=0}

local DISABLE_SCROLLGROUP_PCALL <const> = false

local frame_width_cache = nil

-- [Renderer Function References]
local truncate_text <const> = renderer.truncate_text
local draw_rect <const> = renderer.draw_rect
local draw_rect_ex <const> = renderer.draw_rect_ex
local draw_text <const> = renderer.draw_text
local get_visual_y <const> = renderer.get_visual_y
local is_item_visible <const> = renderer.is_item_visible
local get_color <const> = renderer.get_color
local s_func <const> = renderer.s
local vn_func <const> = renderer.vn
local is_mouse_in <const> = input_module.is_mouse_in

-- [Helper Functions]

local string_sub <const> = string.sub
local function get_stable_id(label)
    if not label then return "unknown" end
    local s, e = string_find(label, "###", 1, true)
    if s then return string_sub(label, e + 1) end
    return label
end

local function get_display_label(label)
    if not label then return "" end
    local s = string_find(label, "###", 1, true)
    if s then return string_sub(label, 1, s - 1) end
    return label
end

-- [Layout Helpers]

function layout.GetWidth()
    local ui = state.ui
    if ui.width_override then return ui.width_override end
    if frame_width_cache then return frame_width_cache end

    local config_sys = state.deps.config_sys
    local cfg_current = config_sys.data.current
    frame_width_cache = cfg_current.w - cfg_current.sidebar_w - cfg_current.padding - s_func(8)
    return frame_width_cache
end

function layout.OverrideWidth(w)
    state.ui.width_override = w
end

-- [Background Image]

function layout.refresh_background()
    local ui = state.ui
    local config_sys = state.deps.config_sys

    if not config_sys or not config_sys.data then
        ui.bg_texture = nil; ui.bg_path_loaded = nil
        return
    end

    local cfg_data = config_sys.data
    if not cfg_data.bg_image_enabled then
        ui.bg_texture = nil; ui.bg_path_loaded = nil
        return
    end

    local filename = cfg_data.bg_image_file
    if not filename or filename == "" then ui.bg_texture = nil; return end

    local full_path = renderer.get_assets_dir() .. "Images\\" .. filename
    if failed_images[full_path] then return end
    if ui.bg_texture and ui.bg_path_loaded == full_path then return end

    if gui and gui.load_image then
        local status, tex = pcall(gui.load_image, full_path)
        if status and tex then
            ui.bg_texture = tex; ui.bg_path_loaded = full_path
        else
            ui.bg_texture = nil; ui.bg_path_loaded = nil
            failed_images[full_path] = true
        end
    end
end

function layout.clear_failed_images()
    failed_images = {}
    failed_sidebar_textures = {}
end

-- [Main Window]

function layout.draw_window(title, content_callback)
    local ui = state.ui
    local config_sys = state.deps.config_sys
    local debugger = state.deps.debugger

    if not ui.open then return end

    frame_width_cache = nil

    local debug_enabled = debugger and debugger.get_config and debugger.get_config().enabled
    if debug_enabled then debugger.mark_start("ui_draw_window") end

    renderer.update_scale()
    local cfg = config_sys.data
    local cfg_current = cfg.current
    if not cfg_current.w then
        if debug_enabled then debugger.mark_end("ui_draw_window") end
        return
    end

    for k in pairs(ui.post_draw_queue) do ui.post_draw_queue[k] = nil end
    ui.layout_counter = 0
    ui.scroll_consumed = false
    ui.subtab_offset = 0

    local delta_time = (game and game.delta and game.delta()) or 0.016
    if delta_time <= 0 then delta_time = 0.016 end
    ui.delta_time = delta_time

    input_module.update_mouse_state()
    input_module.block_game_controls()

    local x, y = cfg.base.x, cfg.base.y
    local w, h = cfg_current.w, cfg_current.h
    local hh = cfg_current.header_h
    local sw = cfg_current.sidebar_w

    input_module.handle_dragging(x, y, w, hh)

    local content_rect = { x = x + sw, y = y + hh, w = w - sw, h = h - hh }

    local content_padding_y = cfg_current.content_padding_y or s_func(8)
    local clip_y = content_rect.y + content_padding_y
    local clip_h = content_rect.h - (content_padding_y * 2)

    ui.visible_h = clip_h
    ui.header_end_y = clip_y
    ui.clip_rect = { x = content_rect.x, y = clip_y, w = content_rect.w, h = clip_h }
    ui.is_clipped = false

    layout.refresh_background()

    local r_tl = s_func(cfg.base.window_rounding_tl or 12)
    local r_tr = s_func(cfg.base.window_rounding_tr or 12)
    local r_bl = s_func(cfg.base.window_rounding_bl or 12)
    local r_br = s_func(cfg.base.window_rounding_br or 12)

    -- Calculate max radius and flags
    local max_r = math_max(r_tl, r_tr, r_bl, r_br)
    local r_flags = 0
    if r_tl > 0 then r_flags = r_flags + gui.rounding.top_left end
    if r_tr > 0 then r_flags = r_flags + gui.rounding.top_right end
    if r_bl > 0 then r_flags = r_flags + gui.rounding.bottom_left end
    if r_br > 0 then r_flags = r_flags + gui.rounding.bottom_right end

    -- Layer 1: Window Background (single draw call)
    if ui.bg_texture and gui.image_rounded then
        local bg_col = get_color("Background Tint")
        local user_alpha = cfg.bg_image_opacity or 255
        if user_alpha < 255 then bg_col = color(bg_col.r, bg_col.g, bg_col.b, user_alpha) end
        gui.image_rounded(ui.bg_texture, vec2(x,y), vec2(w,h), bg_col, max_r, r_flags)
    else
        draw_rect(x, y, w, h, "Window Background", max_r, r_flags)
    end

    -- Layer 2: Header Background (simplified with flags)
    local h_r_tl = s_func(cfg.base.header_rounding_tl or 12)
    local h_r_tr = s_func(cfg.base.header_rounding_tr or 12)
    local h_max_r = math_max(h_r_tl, h_r_tr)
    local h_flags = 0
    if h_r_tl > 0 then h_flags = h_flags + gui.rounding.top_left end
    if h_r_tr > 0 then h_flags = h_flags + gui.rounding.top_right end
    draw_rect(x, y, w, hh, "Header Background", h_max_r, h_flags)

    -- Header Title
    local f_header = vn_func(cfg_current.font_header) > 0 and cfg_current.font_header or 17
    draw_text(title, x, y, f_header, "Header Text", w, hh)

    -- Divider (using gui.line instead of thin rect)
    local div_w = cfg_current.divider_width or s_func(1)
    local div_padding_y = cfg_current.divider_padding or s_func(8)
    local gap_start = x + sw - (cfg_current.sidebar_btn_margin or s_func(6))
    local gap_end = x + sw + (cfg_current.padding * 0.5)
    local div_x = (gap_start + gap_end) * 0.5
    local div_y1 = y + hh + div_padding_y
    local div_y2 = y + h - div_padding_y
    renderer.draw_line(div_x, div_y1, div_x, div_y2, div_w, "Group Header Line")

    -- Content Setup
    ui.cx = content_rect.x + (cfg_current.padding * 0.5)
    ui.window_y_start = clip_y + (cfg_current.window_content_offset or s_func(2))
    ui.cy = 0
    ui.width_override = nil

    if content_callback then
        local ok, err = pcall(content_callback)
        if not ok and debugger then debugger.error("[UI] Window Draw Error: " .. tostring(err)) end
    end

    -- Scrollbar Logic (Main Window)
    local adjusted_clip_y = clip_y + (ui.subtab_offset or 0)
    local adjusted_clip_h = clip_h - (ui.subtab_offset or 0)
    local max_scroll = math_max(0, vn_func(ui.content_h) - vn_func(ui.visible_h) + s_func(20))
    local sb_w = cfg_current.scrollbar_width or s_func(6)

    if max_scroll > 0 then
        local N_DIS = state.natives_cache.IS_DISABLED_CONTROL_JUST_PRESSED

        if is_mouse_in(content_rect.x, content_rect.y, content_rect.w, content_rect.h)
           and N_DIS
           and not ui.scroll_consumed
           and not ui.active_combo
        then
            if N_DIS(0, 241) then ui.scroll_target = ui.scroll_target - (cfg_current.scroll_speed or s_func(60))
            elseif N_DIS(0, 242) then ui.scroll_target = ui.scroll_target + (cfg_current.scroll_speed or s_func(60)) end
        end

        local sb_hit_w = math_max(sb_w, cfg_current.scrollbar_hit_width or s_func(16))
        local is_hovering = is_mouse_in(content_rect.x + content_rect.w - sb_w - s_func(2) - (sb_hit_w - sb_w), adjusted_clip_y, sb_hit_w, adjusted_clip_h)

        if ui.input.mouse_left_pressed and is_hovering and not ui.active_id and not ui.active_combo then
            ui.active_id = "window_scrollbar"
        end

        if ui.active_id == "window_scrollbar" then
            if not ui.input.mouse_left_pressed then ui.active_id = nil
            else
                local ratio = (ui.input.mouse_pos.y - adjusted_clip_y) / adjusted_clip_h
                ui.scroll_target = math_max(0, math_min(1, ratio)) * max_scroll
            end
        end
    end

    if ui.scroll_target < 0 then ui.scroll_target = 0 elseif ui.scroll_target > max_scroll then ui.scroll_target = max_scroll end
    local smooth = 1.0 - math_exp(-10.0 * delta_time)
    ui.scroll_y = vn_func(ui.scroll_y) + (vn_func(ui.scroll_target) - vn_func(ui.scroll_y)) * smooth

    if max_scroll > 0 then
        local ratio = ui.visible_h / (ui.content_h + s_func(20))
        if ratio > 1 then ratio = 1 end
        local kh = math_max(cfg_current.scrollbar_thumb_min or s_func(20), adjusted_clip_h * ratio)
        local ky = adjusted_clip_y + ((adjusted_clip_h - kh) * (ui.scroll_y / max_scroll))
        local sb_x = content_rect.x + content_rect.w - sb_w - s_func(2)
        local sb_col = (ui.active_id == "window_scrollbar") and "Scrollbar Hover" or "Scrollbar"
        draw_rect(sb_x, ky, sb_w, kh, sb_col, s_func(2))
    end

    -- Post-Draw & Cleanup
    for _, item in ipairs(ui.post_draw_queue) do
        if type(item) == "function" then item()
        elseif item.func then item.func(item.args) end
    end
    
    -- Animation Cleanup
    local now = os_clock()
    for i = #ui.click_animations, 1, -1 do
        local anim = ui.click_animations[i]
        local progress = (now - anim.start_time) / (cfg_current.click_anim_duration or 0.25)
        if progress >= 1 then table_remove(ui.click_animations, i)
        else
            anim_color_cache.a = math_floor((1 - progress) * (cfg_current.click_anim_alpha or 40))
            draw_rect_ex(anim.x, anim.y, anim.w, anim.h, nil, cfg_current.widget_rounding, nil, nil, anim_color_cache)
        end
    end

    if ui.is_clipped and gui.pop_clip then gui.pop_clip(); ui.is_clipped = false end
    ui.content_h = ui.cy

    -- Draw Menu Hints Overlay (keyboard shortcuts)
    layout.draw_menu_hints()

    if debug_enabled then debugger.mark_end("ui_draw_window") end
end

-- [Sidebar & SubTabs]

function layout.Sidebar(tabs)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local x, y, hh = state.deps.config_sys.data.base.x, state.deps.config_sys.data.base.y, cfg.header_h
    local sw = cfg.sidebar_w
    local start_y = y + hh + (cfg.content_padding_y or s_func(8))
    local btn_h = sw - (cfg.sidebar_btn_margin or s_func(6)) * 2
    local icons = state.deps.config_sys.data.sidebar_icons or {}

    for i, label in ipairs(tabs) do
        local btn_y = start_y + ((i - 1) * (btn_h + (cfg.sidebar_btn_spacing or s_func(4))))
        local active = (ui.active_tab == i)
        local hover = is_mouse_in(x, btn_y, sw, btn_h)

        draw_rect(x + (cfg.sidebar_btn_margin or s_func(6)), btn_y, btn_h, btn_h, active and "Tab Background Active" or (hover and "Tab Background Hover" or "Tab Background"), cfg.sidebar_btn_rounding)
        
        local img = icons[i]
        local drawn = false
        if img and img ~= "None" and gui.load_image and not failed_sidebar_textures[img] then
             if not ui.sidebar_textures[i] then
                 local path = renderer.get_assets_dir() .. "Images\\" .. img
                 local s, t = pcall(gui.load_image, path)
                 if s and t then
                     ui.sidebar_textures[i] = t
                 else
                     failed_sidebar_textures[img] = true  -- [PERF OPT] Mark failed, prevent retry
                 end
             end
             if ui.sidebar_textures[i] then
                 local isz = cfg.sidebar_icon_size or s_func(32)
                 gui.image(ui.sidebar_textures[i], vec2(x+(sw-isz)*0.5, btn_y+(btn_h-isz)*0.5), vec2(isz,isz), get_color(active and "Tab Text Active" or "Text Primary"))
                 drawn = true
             end
        end
        if not drawn then
             draw_text(label, x, btn_y, cfg.font_tab or 22, active and "Tab Text Active" or "Tab Text Inactive", sw, btn_h)
        end
        
        if hover and ui.input.mouse_left_just_pressed then
            ui.active_tab = i; ui.scroll_y = 0; ui.scroll_target = 0
        end
    end
    
    if gui.push_clip and not ui.is_clipped then
        gui.push_clip(vec2(ui.clip_rect.x, ui.clip_rect.y), vec2(ui.clip_rect.w, ui.clip_rect.h))
        ui.is_clipped = true
    end
end

function layout.SubTabBar(options, current_idx)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current

    if not options then return current_idx or 1 end

    local w = layout.GetWidth()
    local h = cfg.subtab_height or s_func(30)
    local font_size = cfg.font_subtab or 13

    local is_in_scroll = ui.in_scroll_context

    local vy
    if is_in_scroll then
        -- Inside ScrollGroup: use scroll position
        vy = get_visual_y()
    else
        -- Top level or inside Group: fixed at content area top
        vy = ui.header_end_y

        if ui.is_clipped and gui.pop_clip then
            gui.pop_clip()
            ui.is_clipped = false
        end
    end

    -- Render SubTabBar
    local count = #options
    if count == 0 then
        return current_idx
    end
    local gap = cfg.subtab_gap or s_func(8)
    local tab_w = (w - (count-1)*gap) / count
    local padding = cfg.subtab_text_padding or s_func(8)
    local max_text_w = tab_w - padding

    -- Visibility check (only needed inside ScrollGroup)
    local should_draw = true
    if is_in_scroll then
        should_draw = is_item_visible(vy, h)
    end

    if should_draw then
        for i, name in ipairs(options) do
            local tx = ui.cx + (i-1)*(tab_w+gap)
            local active = (i == current_idx)
            local hover = is_mouse_in(tx, vy, tab_w, h)
            draw_rect(tx, vy, tab_w, h, active and "Subtab Active" or (hover and "Subtab Hover" or "Subtab Background"), cfg.subtab_rounding)
            local truncated = truncate_text(name, font_size, max_text_w)
            draw_text(truncated, tx, vy, font_size, active and "Subtab Text Active" or "Subtab Text Inactive", tab_w, h)
            if hover and ui.input.mouse_left_just_pressed then current_idx = i end
        end
    end

    -- Calculate SubTabBar total height
    local subtab_total_h = h + (cfg.subtab_spacing_after or s_func(6))

    if not is_in_scroll then
        if gui.push_clip then
            local new_clip_y = ui.clip_rect.y + subtab_total_h
            local new_clip_h = ui.clip_rect.h - subtab_total_h
            if new_clip_h > 0 then
                gui.push_clip(renderer.get_vec2(ui.clip_rect.x, new_clip_y), renderer.get_vec2(ui.clip_rect.w, new_clip_h))
                ui.is_clipped = true
                ui.subtab_offset = subtab_total_h
            end
        end
    end

    -- Update cy
    if ui.cy < subtab_total_h then
        ui.cy = subtab_total_h
    end
    return current_idx
end

-- [SubTabBar2 - Nested SubTab Component]

--- Nested SubTabBar component (renders below main SubTabBar)
--- @param options table Array of tab names
--- @param current_idx number Currently selected tab index (1-based)
--- @return number Updated selected tab index
function layout.SubTabBar2(options, current_idx)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current

    if not options then return current_idx or 1 end
    local w = layout.GetWidth()
    local h = cfg.subtab_height or s_func(30)
    local font_size = cfg.font_subtab or 13

    -- [KEY DIFFERENCE] Position calculation uses ui.subtab_offset for nesting
    -- Main SubTabBar: vy = ui.header_end_y
    -- Nested SubTabBar2: vy = ui.header_end_y + ui.subtab_offset
    local vy = ui.header_end_y + (ui.subtab_offset or 0)

    if ui.is_clipped and gui.pop_clip then
        gui.pop_clip()
        ui.is_clipped = false
    end

    -- Render SubTabBar2
    local count = #options
    if count == 0 then
        return current_idx
    end
    local gap = cfg.subtab_gap or s_func(8)
    local tab_w = (w - (count-1)*gap) / count
    local padding = cfg.subtab_text_padding or s_func(8)
    local max_text_w = tab_w - padding

    for i, name in ipairs(options) do
        local tx = ui.cx + (i-1)*(tab_w+gap)
        local active = (i == current_idx)
        local hover = is_mouse_in(tx, vy, tab_w, h)
        draw_rect(tx, vy, tab_w, h, active and "Subtab Active" or (hover and "Subtab Hover" or "Subtab Background"), cfg.subtab_rounding)
        local truncated = truncate_text(name, font_size, max_text_w)
        draw_text(truncated, tx, vy, font_size, active and "Subtab Text Active" or "Subtab Text Inactive", tab_w, h)
        if hover and ui.input.mouse_left_just_pressed then current_idx = i end
    end

    -- Calculate total height
    local subtab_total_h = h + (cfg.subtab_spacing_after or s_func(6))

    -- [KEY DIFFERENCE] Accumulate offset instead of overwriting
    local new_offset = (ui.subtab_offset or 0) + subtab_total_h

    -- Push new clip region from SubTabBar2 bottom
    if gui.push_clip then
        local new_clip_y = ui.clip_rect.y + new_offset
        local new_clip_h = ui.clip_rect.h - new_offset
        if new_clip_h > 0 then
            gui.push_clip(renderer.get_vec2(ui.clip_rect.x, new_clip_y), renderer.get_vec2(ui.clip_rect.w, new_clip_h))
            ui.is_clipped = true
            ui.subtab_offset = new_offset
        end
    end

    -- Update cy
    if ui.cy < new_offset then
        ui.cy = new_offset
    end

    return current_idx
end

function layout.DualColumn(l, r)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local sy, scx = ui.cy, ui.cx
    local gap = cfg.dual_column_gap or s_func(8)
    local w = (layout.GetWidth() - gap) * 0.5
    layout.OverrideWidth(w); if l then l() end; local ly = ui.cy
    ui.cy = sy; ui.cx = scx + w + gap; layout.OverrideWidth(w); if r then r() end; local ry = ui.cy
    ui.cx = scx; ui.cy = math_max(ly, ry) + (cfg.dual_column_bottom_margin or s_func(10)); layout.OverrideWidth(nil)
end

-- [Fixed Left Column] Left column does not scroll with window, right column scrolls normally
function layout.DualColumn_FixedLeft(l, r)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local sy, scx = ui.cy, ui.cx
    local gap = cfg.dual_column_gap or s_func(8)
    local w = (layout.GetWidth() - gap) * 0.5

    -- Left column: temporarily disable window scrolling
    local saved_scroll = ui.scroll_y
    ui.scroll_y = 0
    layout.OverrideWidth(w)
    if l then l() end
    ui.scroll_y = saved_scroll

    -- Right column: normal scrolling
    ui.cy = sy
    ui.cx = scx + w + gap
    layout.OverrideWidth(w)
    if r then r() end
    local ry = ui.cy

    ui.cx = scx
    -- Use only right column height for window content height, left column is fixed
    ui.cy = ry + (cfg.dual_column_bottom_margin or s_func(10))
    layout.OverrideWidth(nil)
end

function layout.Group(label, content, args)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local vy = get_visual_y()
    local w = layout.GetWidth()
    local content_pad = cfg.group_content_padding or s_func(10)
    local group_spacing = cfg.group_spacing or s_func(8)
    local group_rounding = cfg.group_rounding or s_func(8)
    local header_font = cfg.font_group or 14

    -- [Hidden title support] Check if label starts with "##" to hide title
    local is_hidden_title = string_find(label, "^##") ~= nil
    local header_h = is_hidden_title and s_func(10) or (cfg.group_header_height or s_func(36))

    ui.layout_counter = ui.layout_counter + 1
    local id = "grp_" .. label
    local ch = ui.layout_cache[id]

    -- Culling (only with valid cache)
    if ch and not is_item_visible(vy, ch) and not ui.active_id then
        ui.cy = ui.cy + ch + group_spacing
        return
    end

    -- Use cached height for background, or large estimate if no cache
    local bg_h = ch or (header_h + s_func(800))  -- Large estimate covers tall Groups
    draw_rect(ui.cx, vy, w, bg_h, "Group Background", group_rounding)

    -- [Hidden title support] Only draw title if not hidden
    if not is_hidden_title then
        draw_text(label, ui.cx + content_pad, vy, header_font, "Group Header Text", nil, header_h)
    end

    local sy = ui.cy
    ui.cy = ui.cy + header_h
    local ox, ow = ui.cx, ui.width_override
    ui.cx = ui.cx + content_pad
    layout.OverrideWidth(w - (content_pad * 2))

    -- Push ID context for unique widget IDs within this Group
    state.push_id_context(label)

    if content then if args then content(args) else content() end end

    -- Pop ID context when leaving Group
    state.pop_id_context()

    ui.cx = ox; layout.OverrideWidth(ow)

    local total_h = (ui.cy - sy) + content_pad
    ui.layout_cache[id] = total_h
    ui.cy = sy + total_h + group_spacing
end

-- [Collapsible]

--- Collapsible component: click title bar to expand/collapse content
--- @param label string Title text (also used as unique ID)
--- @param content function Content rendering function
--- @param args any|nil Arguments passed to content
--- @param default_expanded boolean|nil Whether expanded by default (default false)
--- @return boolean Current expanded state
function layout.Collapsible(label, content, args, default_expanded)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local vy = get_visual_y()
    local w = layout.GetWidth()
    local header_h = cfg.group_header_height or s_func(36)
    local content_pad = cfg.group_content_padding or s_func(10)
    local group_spacing = cfg.group_spacing or s_func(8)
    local group_rounding = cfg.group_rounding or s_func(8)
    local header_font = cfg.font_group or 14

    -- Get/initialize expanded state
    local id = label
    if ui.collapsible_states[id] == nil then
        ui.collapsible_states[id] = default_expanded or false
    end
    local expanded = ui.collapsible_states[id]

    ui.layout_counter = ui.layout_counter + 1
    local cache_id = "col_" .. label
    local cached_content_h = ui.layout_cache[cache_id] or 0
    local total_h = expanded and (header_h + cached_content_h + content_pad) or header_h

    -- Visibility culling
    if not is_item_visible(vy, total_h) and not ui.active_id then
        ui.cy = ui.cy + total_h + group_spacing
        return expanded
    end

    -- Draw background
    draw_rect(ui.cx, vy, w, total_h, "Group Background", group_rounding)

    -- Draw title bar (clickable)
    local header_hover = is_mouse_in(ui.cx, vy, w, header_h)
    if header_hover and not ui.active_combo then
        draw_rect(ui.cx, vy, w, header_h, "Widget Hover", group_rounding)
    end

    -- Draw expand/collapse arrow
    local arrow = expanded and "v" or ">"
    local arrow_x = ui.cx + content_pad
    local arrow_font = (header_font or 14) + 4
    draw_text(arrow, arrow_x, vy, arrow_font, "Accent", nil, header_h)

    -- Draw title text
    local title_x = arrow_x + s_func(18)
    draw_text(label, title_x, vy, header_font, header_hover and "Text Primary" or "Group Header Text", nil, header_h)

    -- Toggle expanded state on click
    if header_hover and ui.input.mouse_left_just_pressed and not ui.active_combo then
        expanded = not expanded
        ui.collapsible_states[id] = expanded
    end

    -- Draw content (only when expanded)
    if expanded and content then
        local sy = ui.cy
        ui.cy = ui.cy + header_h
        local ox, ow = ui.cx, ui.width_override
        ui.cx = ui.cx + content_pad
        layout.OverrideWidth(w - (content_pad * 2))

        if args then content(args) else content() end

        ui.cx = ox
        layout.OverrideWidth(ow)

        -- Cache content height
        local content_height = ui.cy - sy - header_h
        ui.layout_cache[cache_id] = content_height

        total_h = header_h + content_height + content_pad
        ui.cy = sy + total_h + group_spacing
    else
        ui.cy = ui.cy + header_h + group_spacing
    end

    return expanded
end

-- [ScrollGroup - Auto-Fill Height]

function layout.ScrollGroup(label, fixed_h, content_func)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local vy = get_visual_y()
    local w = layout.GetWidth()
    local content_pad = cfg.group_content_padding or s_func(10)
    local group_spacing = cfg.group_spacing or s_func(8)
    local header_h = cfg.group_header_height or s_func(36)

    -- [FEATURE] Auto-height mode: dynamically adjusts based on content
    -- "auto" = shrinks when content is small, expands when content is large (up to available space)
    -- -1 = always fills to bottom of window (legacy behavior)
    local stable_id = get_stable_id(label)
    local render_h = fixed_h
    local is_auto = (fixed_h == "auto")

    if is_auto then
        local cache_id = "scr_h_" .. stable_id
        local bottom_y = ui.header_end_y + ui.visible_h
        local max_avail = math_max(s_func(100), bottom_y - vy)
        local last_content_h = ui.layout_cache[cache_id] or s_func(50)
        local needed = last_content_h + header_h + content_pad + s_func(15)
        render_h = math_min(math_max(s_func(100), needed), max_avail)
    elseif fixed_h == -1 or (label and string_find(label, "grp_online_players")) then
        -- Legacy: fill to bottom
        local bottom_y = ui.header_end_y + ui.visible_h
        local remaining = bottom_y - vy
        render_h = math_max(s_func(100), remaining)
    end

    -- Visibility Culling
    ui.layout_counter = ui.layout_counter + 1
    if not is_item_visible(vy, render_h) and not ui.active_id then
        ui.cy = ui.cy + render_h + group_spacing
        return
    end
    draw_rect(ui.cx, vy, w, render_h, "Group Background", cfg.group_rounding or s_func(8))
    draw_text(get_display_label(label), ui.cx + content_pad, vy, cfg.font_group, "Group Header Text", nil, header_h)

    local content_y = vy + header_h
    local view_h = render_h - header_h - content_pad
    local state_id = get_stable_id(label) .. "_scroll"
    
    if not ui.scroll_states[state_id] then ui.scroll_states[state_id] = { y=0, target=0, content_h=0 } end
    local s_state = ui.scroll_states[state_id]
    local max_s = math_max(0, s_state.content_h - view_h)

    -- Input
    if is_mouse_in(ui.cx, content_y, w, view_h) and not ui.active_combo then
         local N_DIS = state.natives_cache.IS_DISABLED_CONTROL_JUST_PRESSED
         if N_DIS and N_DIS(0, 241) then s_state.target = s_state.target - (cfg.scroll_group_speed or s_func(40)); ui.scroll_consumed = true end
         if N_DIS and N_DIS(0, 242) then s_state.target = s_state.target + (cfg.scroll_group_speed or s_func(40)); ui.scroll_consumed = true end
    end
    
    -- Drag Scrollbar
    local sb_w = cfg.scrollbar_width or s_func(6)
    local bar_x = ui.cx + w - sb_w - s_func(2)
    local sb_id = state_id .. "_drag"

    local sb_hit_w = math_max(sb_w, cfg.scrollbar_hit_width or s_func(16))
    local sb_hit_x = bar_x - (sb_hit_w - sb_w)
    if ui.input.mouse_left_pressed and is_mouse_in(sb_hit_x, content_y, sb_hit_w, view_h) and not ui.active_id and not ui.active_combo then
        ui.active_id = sb_id
    end
    if ui.active_id == sb_id then
        if not ui.input.mouse_left_pressed then ui.active_id = nil
        else
            local r = (ui.input.mouse_pos.y - content_y) / view_h
            s_state.target = math_max(0, math_min(1, r)) * max_s
        end
    end
    
    s_state.target = math_max(0, math_min(s_state.target, max_s))
    s_state.y = s_state.y + (s_state.target - s_state.y) * (1.0 - math_exp(-10.0 * ui.delta_time))

    -- Clip & Render
    if gui.push_clip then gui.push_clip(renderer.get_vec2(ui.cx, content_y), renderer.get_vec2(w, view_h)) end
    
    local oy, ox, ow = ui.cy, ui.cx, ui.width_override
    ui.cx = ui.cx + content_pad
    layout.OverrideWidth(w - (content_pad * 2))
    ui.cy = ui.cy + header_h - s_state.y

    if content_func then
        local was_in_scroll = ui.in_scroll_context
        ui.in_scroll_context = true
        if DISABLE_SCROLLGROUP_PCALL then
            content_func()
        else
            local status, err = pcall(content_func)
            if not status and state.deps.debugger then
                state.deps.debugger.error("[Layout]", "ScrollGroup Error: " .. tostring(err))
            end
        end
        ui.in_scroll_context = was_in_scroll
    end

    s_state.content_h = (ui.cy + s_state.y) - (oy + header_h)

    if is_auto then
        local cache_id = "scr_h_" .. stable_id
        ui.layout_cache[cache_id] = s_state.content_h
    end

    ui.cx = ox; layout.OverrideWidth(ow)
    if gui.pop_clip then gui.pop_clip() end
    
    -- Draw Bar
    if max_s > 0 then
        local r = view_h / s_state.content_h
        local bh = math_max(cfg.scrollbar_thumb_min or s_func(20), view_h * math_min(1, r))
        local by = content_y + ((view_h - bh) * (s_state.y / max_s))
        draw_rect(bar_x, by, sb_w, bh, (ui.active_id == sb_id) and "Scrollbar Hover" or "Scrollbar", s_func(2))
    end
    
    ui.cy = oy + render_h + group_spacing
end

-- [Row Layout]

--- Iterative Row - avoids closure creation per item
--- @param count number Number of items
--- @param callback function Called with index (1-based) for each item
--- @param options table|nil Optional: {gap=8}
function layout.Row_Iterative(count, callback, options)
    if count <= 0 then return end

    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local gap = (options and options.gap) or cfg.row_gap or s_func(8)

    local start_cx, start_cy = ui.cx, ui.cy
    local original_override = ui.width_override
    local max_cy = start_cy

    local total_w = layout.GetWidth()
    local item_w = (total_w - (count - 1) * gap) / count

    for i = 1, count do
        ui.cy = start_cy
        ui.cx = start_cx + (i - 1) * (item_w + gap)
        layout.OverrideWidth(item_w)
        callback(i)  -- Direct call, no closure per item
        if ui.cy > max_cy then max_cy = ui.cy end
    end

    ui.cx = start_cx
    ui.cy = max_cy
    layout.OverrideWidth(original_override)
end

--- Row layout for placing multiple widgets horizontally
--- @param items table Array of functions, each rendering one widget
--- @param options table|nil Optional: {gap=8, equal_width=true}
function layout.Row(items, options)
    if not items or #items == 0 then return end

    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local count = #items

    -- Options
    options = options or {}
    local gap = options.gap or cfg.row_gap or s_func(8)
    local equal_width = (options.equal_width ~= false) -- Default true

    -- Calculate widths
    local total_w = layout.GetWidth()
    local item_w

    if equal_width then
        item_w = (total_w - (count - 1) * gap) / count
    end

    -- Save state
    local start_cy = ui.cy
    local start_cx = ui.cx
    local original_override = ui.width_override
    local max_cy = start_cy

    -- Render each item
    for i, render_func in ipairs(items) do
        ui.cy = start_cy
        ui.cx = start_cx + (i - 1) * (item_w + gap)

        if equal_width then
            layout.OverrideWidth(item_w)
        end

        if render_func then
            render_func()
        end

        -- Track max height
        if ui.cy > max_cy then
            max_cy = ui.cy
        end
    end

    -- Restore state
    ui.cx = start_cx
    ui.cy = max_cy
    layout.OverrideWidth(original_override)
end

--- ButtonRow - avoids closure allocation per call
--- @param labels table Array of button labels
--- @param options table|nil Optional: {gap=8}
--- @return number|nil Index of clicked button (1-based), or nil if none clicked
function layout.ButtonRow(labels, options)
    if not labels or #labels == 0 then return nil end
    if not widgets then widgets = require("ClickUl.UI.widgets") end

    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local h = cfg.item_h
    local vy = get_visual_y()

    if not is_item_visible(vy, h) then
        ui.cy = ui.cy + h + (cfg.row_gap or s_func(8))
        return nil
    end

    local count = #labels
    local gap = (options and options.gap) or cfg.row_gap or s_func(8)
    local start_cx, start_cy = ui.cx, ui.cy
    local original_override = ui.width_override
    local max_cy = start_cy
    local total_w = layout.GetWidth()
    local item_w = (total_w - (count - 1) * gap) / count

    local clicked_idx = nil

    for i = 1, count do
        ui.cy = start_cy
        ui.cx = start_cx + (i - 1) * (item_w + gap)
        layout.OverrideWidth(item_w)

        -- Direct widget call
        if widgets.Button(labels[i]) then
            clicked_idx = i
        end

        if ui.cy > max_cy then max_cy = ui.cy end
    end

    ui.cx = start_cx
    ui.cy = max_cy
    layout.OverrideWidth(original_override)

    return clicked_idx
end

-- [Menu Hints Overlay]

--- Draw menu hints overlay (keyboard shortcuts)
--- Shows fixed window below main menu when hovering over widgets with hints
function layout.draw_menu_hints()
    local ui = state.ui
    local hint_state = ui.menu_hint
    if not hint_state or not hint_state.current then return end

    -- Auto-timeout: clear after 0.1s without update (prevents hint from sticking)
    if os_clock() - hint_state.last_update > 0.1 then
        hint_state.current = nil
        return
    end

    -- Calculate position (below main window)
    local cfg = state.deps.config_sys.data
    local base = cfg.base
    local current = cfg.current

    local ox = base.x
    local oy = base.y + current.h + (current.menu_hints_offset_y or s_func(2))
    local width = current.w
    local height = current.menu_hints_height or s_func(30)
    local padding = current.menu_hints_padding or s_func(10)
    local rounding = current.menu_hints_rounding or s_func(8)

    -- Draw background
    if gui and gui.rect then
        local bg_color = get_color("Menu Hints Background")
        gui.rect(renderer.get_vec2(ox, oy), renderer.get_vec2(width, height))
           :filled():color(bg_color):rounding(rounding):draw()
    end

    -- Draw hint text
    if gui and gui.text then
        local text_color = get_color("Menu Hints Text")
        local font_size = current.menu_hints_font_size or s_func(14)
        gui.text(hint_state.current)
           :position(renderer.get_vec2(ox + padding, oy + (height - font_size) * 0.5))
           :color(text_color):scale(font_size):draw()
    end
end

return layout
